package com.geek.gkfgw.util.search;

import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Annotation;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * <h1>类搜索工具类。</h1><br/>
 *
 * <h1><br/><a href="https://u.geekbang.org/subject/arch3/100614201">Java 业务架构实战营：https://u.geekbang.org/subject/arch3/100614201</a></h1><br/><h1><a href="https://gitee.com/ylimhhmily/BizArchPracticeCamp">Java 业务架构实战营代码地址：https://gitee.com/ylimhhmily/BizArchPracticeCamp</a></h1><br/><h1><a href="https://u.geekbang.org/subject/java4th/1001148?source=app_share">高级 Java 工程体系课 2023 版：https://u.geekbang.org/subject/java4th/1001148?source=app_share</a></h1><br/><h1><a href="https://time.geekbang.org/column/intro/100312101?tab=catalog">Dubbo 源码剖析与实战：https://time.geekbang.org/column/intro/100312101?tab=catalog</a></h1>
 *
 * @author hmilyylimh
 *         ^_^
 * @version 0.0.1
 *         ^_^
 * @date 2022-02-14
 *
 */
@Slf4j
public class ClassSearchUtils {

    public static List<Class> search(String pkg, Class<? extends Annotation>[] interestAnnoClzList){
        final List<Class> classes = new ArrayList<>();
        try {
            pkg = pkg.replace('.', '/');
            Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources(pkg);

            for (URL url ; resources.hasMoreElements() && ((url = resources.nextElement()) != null);) {
                try {
                    String protocol = url.getProtocol();
                    if(protocol.equalsIgnoreCase("jar")) {
                        checkJarFile(url, pkg, classes, interestAnnoClzList);
                    } else if(protocol.equalsIgnoreCase("file")) {
                        try {
                            checkDirectory(new File(URLDecoder.decode(url.getPath(), "UTF-8")), pkg, classes, interestAnnoClzList);
                        } catch (final UnsupportedEncodingException ex) {
                            throw new ClassNotFoundException(pkg + " does not appear to be a valid package (Unsupported encoding)", ex);
                        }
                    } else {
                        throw new ClassNotFoundException(pkg + " (" + url.getPath() + ") does not appear to be a valid package");
                    }
                } catch (final IOException ioex) {
                    log.error("search IOException.", ioex);
                    throw new ClassNotFoundException("IOException was thrown when trying to get all resources for " + pkg, ioex);
                }
            }


        } catch (Exception e) {
            log.error("search Exception.", e);
        }

        return classes;
    }

    private static void checkJarFile(URL url, String pkg, List<Class> classes, Class<? extends Annotation>[] interestAnnoClzList) throws IOException {
        JarURLConnection connection = (JarURLConnection) url.openConnection();
        final JarFile jarFile = connection.getJarFile();
        final Enumeration<JarEntry> entries = jarFile.entries();
        String name;
        for (JarEntry jarEntry ; entries.hasMoreElements()
                && ((jarEntry = entries.nextElement()) != null);) {
            name = jarEntry.getName();

            if (name.endsWith(".class")) {
                if (name.startsWith(pkg)) {
                    try{
                        name = name.replace("/",".").replace(".class","");
                        Class<?> clz = Class.forName(name);
                        if (isIllegal(interestAnnoClzList, clz)) {
                            continue;
                        }
                        classes.add(clz);
                    } catch(Exception e) {
                        log.error("checkJarFile Exception.", e);
                    }
                }
            }
        }
    }

    private static void checkDirectory(File directory, String pkg, List<Class> classes, Class<? extends Annotation>[] interestAnnoClzList) throws ClassNotFoundException {
        File tmpDirectory;

        if (directory.exists() && directory.isDirectory()) {
            final String[] files = directory.list();

            for (final String file : files) {
                if (file.endsWith(".class")) {
                    try {
                        String clzFullName = pkg.replace('/', '.') + '.' + file.substring(0, file.length() - 6);
                        Class<?> clz = Thread.currentThread().getContextClassLoader().loadClass(clzFullName);
                        if (isIllegal(interestAnnoClzList, clz)) {
                            continue;
                        }

                        classes.add(clz);
                    } catch (final NoClassDefFoundError e) {
                        // do nothing. this class hasn't been found by the
                        // loader, and we don't care.
                        log.error("checkDirectory NoClassDefFoundError.", e);
                    }
                } else if ((tmpDirectory = new File(directory, file)).isDirectory()) {
                    checkDirectory(tmpDirectory, pkg + "/" + file, classes, interestAnnoClzList);
                }
            }
        }
    }

    /**
     * 是否非法。
     * @param interestAnnoClzList
     * @param clz
     * @return: true: 非法; false: 合法。
     */
    private static boolean isIllegal(Class<? extends Annotation>[] interestAnnoClzList, Class<?> clz) {
        Class<?>[] interfaces = clz.getInterfaces();
        if (interfaces == null || interfaces.length == 0) {
            return true;
        }
        boolean isIllegal = true;
        if (interestAnnoClzList != null) {
            for (Class<?> itemClz : interfaces) {
                if (Arrays.stream(interestAnnoClzList).allMatch(item -> itemClz.isAnnotationPresent(item))) {
                    isIllegal = false;
                    break;
                }
            }
        }
        return isIllegal;
    }
}